#include "Ray_Util.h"
#include <math.h>

Pixel::Pixel(){
	red=0;
	green=0;
	blue=0;
}

Pixel::~Pixel()
{

}

void Pixel::SetColor( float r, float g, float b )
{
	red=r;
	green=g;
	blue=b;
}
Screen::Screen(int width, int height, float windowL, float windowR, float windowT, float windowB){
	screenBuffer=new Pixel[width*height]();
	screenWidth=width;
	screenHeight=height;
	windowLeft=windowL;
	windowRight=windowR;
	windowTop=windowT;
	windowBottom=windowB;
	windowHorizontalStep=(windowR-windowL)/(float)width;
	windowVerticalStep=(windowB-windowT)/(float)height;
}

Screen::~Screen(){
	delete [] screenBuffer;
}

Pixel* Screen::GetScreenBuffer(){
	return screenBuffer;
}

void Ray::SetOrigin( float x,float y, float z )
{
	origin.set(x,y,z);
}

void Ray::SetDirection( float x, float y, float z )
{
	direction.set(x,y,z);
}

void Object::SetType( int geo)
{
	geometryType=geo;	
}

int Object::GetGeometryType()
{
	return geometryType;
}

int Object::GetOpticalType()
{
	return opticalType;
}

Material* Object::GetMaterial()
{
	return &material;
}

void Object::SetLight( int opt )
{
	opticalType=opt;
}
void Material::SetReflaction( float refl )
{
	reflaction=refl;
}

void Material::SetOpticalFactor( float diff, float spec )
{
	diffuse=diff;
	specular=spec;
}

void Material::SetMaterialColor( float r, float g, float b )
{
	color.set(r,g,b);
}

void Material::SetRefraction( float refr )
{
	refraction=refr;
}

void Material::SetRefractionIndex( float index )
{
	refractionIndex=index;
}

void Material::SetDiffuseReflaction( float diffRefl )
{
	diffuseReflaction=diffRefl;
}
Sphere::Sphere( float r, vector3& c, int opt )
{
	center=c;
	radius=r;
	squareRadius=r*r;
	reverseRadius=1.0f/r;
	SetType(SPHERE);
	SetLight(opt);
}

float Sphere::GetRadius()
{
	return radius;
}

float Sphere::GetSquareRadius()
{
	return reverseRadius;
}

vector3& Sphere::GetCenter()
{
	return center;
}

int Sphere::IntersectTest( Ray& ray, float& dist )
{
	vector3 v = ray.origin - center;
	float b = -DotProduct(v,ray.direction);
	float det = (b * b) - DotProduct(v,v) + squareRadius;
	int retval = MISS;
	if (det > 0)
	{
		det = sqrtf( det );
		float i1 = b - det;
		float i2 = b + det;
		if (i2 > 0)
		{
			if (i1 < 0) 
			{
				if (i2 < dist) 
				{
					dist = i2;
					retval = INPRIM;
				}
			}
			else
			{
				if (i1 < dist)
				{
					dist = i1;
					retval = HIT;
				}
			}
		}
	}

	return retval;
}

vector3 Sphere::GetNormal( vector3& point )
{
	return (point - center) * reverseRadius;
}

Plane::Plane( vector3& n, float dist, int opt )
{
	normal=n.normalize();
	distance=dist;
	SetType(PLANE);
	SetLight(opt);
}

float Plane::GetDistance()
{
	return distance;
}

vector3 Plane::GetNormal( vector3& point )
{
	return normal;
}

int Plane::IntersectTest( Ray& ray, float& dist )
{
	float d = DotProduct(normal,ray.direction);
	if (d != 0)
	{
		float dis =-(DotProduct(normal,ray.origin)+distance)/d;
		if (dis > 0)
		{
			if (dis < dist) 
			{
				dist = dis;
				return HIT;
			}
		}
	}
	return MISS;
}

int Scene::GetObjectsNum()
{
	return numObjects;
}

Object* Scene::GetObjectAt( int index )
{
	return objects[index];
}

void Scene::InitScene()
{
	objects=new Object*[100];

	int index=0;

	objects[index]=new Plane(vector3(0,1,0),4.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetMaterialColor(0.75f,0.75f,0.75f);//color.set();
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);//diffuse=1.0f;
	objects[index]->GetMaterial()->SetReflaction(0.0f);//reflaction=0.0f;
	objects[index]->GetMaterial()->SetRefraction(0.0f);

	index++;

	objects[index]=new Sphere(2.0f,vector3(1, -0.8f, 7),NONLIGHT);
	objects[index]->GetMaterial()->SetMaterialColor(1.0f, 1.0f, 1.0f);//color.set(0.7f, 0.7f, 0.7f);
	objects[index]->GetMaterial()->SetReflaction(0.2f);//reflaction=0.6f;
	objects[index]->GetMaterial()->SetRefraction(0.9f);
	objects[index]->GetMaterial()->SetRefractionIndex(1.3f);
	objects[index]->GetMaterial()->SetOpticalFactor(0.0f,1.0f);//diffuse=1.0f;
	objects[index]->GetMaterial()->SetDiffuseReflaction(0.3f);

	index++;

	objects[index]=new Plane(vector3(0,0,-1),12.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.75f,0.75f,0.75f);

	index++;

	objects[index]=new Box(vector3(-1, 5, 4),vector3(2, 0.1f, 2),LIGHT);
	objects[index]->GetMaterial()->SetMaterialColor(1,1,1);

	index++;

	objects[index]=new Plane(vector3(1,0,0),8.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.75f,0.25f,0.25f);

	index++;

	objects[index]=new Plane(vector3(-1,0,0),7.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.25f,0.75f,0.25f);

	index++;

	objects[index]=new Plane(vector3(0,0,1),12.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.75f,0.75f,0.75f);

	index++;

	objects[index]=new Plane(vector3(0,-1,0),14.4f,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.75f,0.75f,0.75f);

	index++;

	vector3 a(-4, -0.8f, 7), b(-4, 0.8f, 7), c(-1, -0.8f, 7);

	objects[index]=new Triangle(a,b,c,NONLIGHT);
	objects[index]->GetMaterial()->SetReflaction(0.0f);
	objects[index]->GetMaterial()->SetRefraction(0.0f);
	objects[index]->GetMaterial()->SetOpticalFactor(1.0f,0.0f);
	objects[index]->GetMaterial()->SetMaterialColor(0.0f,0.75f,0.75f);
	
	index++;

	numObjects=index;

}

Engine::Engine( int width, int height, float windowL, float windowR, float windowT, float windowB )
{
	screen=new Screen(width,height,windowL,windowR,windowT,windowB);
	scene=new Scene();
	scene->InitScene();
	currentLine=0;
	currentX=screen->windowLeft;
	currentY=screen->windowTop;
	scale=1.0f/SAMPLES;
}

Engine::~Engine()
{
	delete screen;
	delete scene;
}

bool Engine::Render()
{
	Pixel* buffer=screen->GetScreenBuffer();
	vector3 eyePosition( 0, 0, -5 );
	clock_t start,end;
	Object* lastObject=0;

	for ( int y = currentLine; y < screen->screenHeight; y++ )
	{
		currentX = screen->windowLeft;
		start=clock();

		for ( int x = 0; x < screen->screenWidth; x++ )
		{

			vector3 pixelColor( 0, 0, 0 );
			vector3 dir = vector3( currentX, currentY, 0 ) - eyePosition;
			dir=dir.normalize();
			Ray ray;
			ray.SetOrigin(eyePosition.x,eyePosition.y,eyePosition.z);
			ray.SetDirection(dir.x,dir.y,dir.z);
			float dist;
			dist=MAXDISTANCE;
			Object* object=RayTrace(ray,pixelColor,1,dist,1.0f);

			if (object!=lastObject)
			{
				lastObject=object;
				vector3 color(0,0,0);

				for (int p=-1;p<2;p++)
				{
					for (int q=-1;q<2;q++)
					{
						vector3 dir=vector3(currentX+screen->windowHorizontalStep*p/2.0f,currentY+screen->windowVerticalStep*q/2.0f,0)-eyePosition;
						dir=dir.normalize();
						Ray ray;
						ray.SetOrigin(eyePosition.x,eyePosition.y,eyePosition.z);
						ray.SetDirection(dir.x,dir.y,dir.z);
						float dist;

						Object* object=RayTrace(ray,color,1,dist,1.0f);
					}
				}

				buffer[y*screen->screenWidth+x].SetColor(color.x/9,color.y/9,color.z/9);
			}else{
				buffer[y*screen->screenWidth+x].SetColor(pixelColor.x,pixelColor.y,pixelColor.z);
			}	

			buffer[y*screen->screenWidth+x].SetColor(pixelColor.x,pixelColor.y,pixelColor.z);
			
			currentX += screen->windowHorizontalStep;
		}
		currentY += screen->windowVerticalStep;

		end=clock();

		if (end-start>10)
		{
			currentLine=y+1;
			return false;
		}
		
	}
	return true;
}

Scene* Engine::GetScene()
{
	return scene;
}

Screen* Engine::GetScreen()
{
	return screen;
}

Object* Engine::RayTrace( Ray& ray, vector3& color, int depth, float& dist, float index)
{
	if (depth > MAXDEPTH) return 0;

	dist=MAXDISTANCE;

	Object* object = 0;
	int result=0;

	for ( int x = 0; x < scene->GetObjectsNum(); x++ )
	{
		Object* obj = scene->GetObjectAt(x);
		int res;
		if (res = obj->IntersectTest(ray,dist))
		{
			object = obj;
			result = res; 
		}
		
	}

	if (!object) return 0;
	

	if (object->GetOpticalType()==LIGHT)
	{
		color = vector3( 1, 1, 1 );		
	}
	else
	{
		
		vector3 point=ray.origin+ray.direction*dist;

		for ( int x = 0; x < scene->GetObjectsNum(); x++ )
		{
			Object* obj = scene->GetObjectAt(x);
			if (obj->GetOpticalType()==LIGHT)
			{
				Object* light = obj;

				vector3 L;

				float shade=CalculateShade(light,point,L);				

				if (shade>0)
				{
					
					vector3 N=object->GetNormal(point);

					if (object->GetMaterial()->diffuse>0)
					{
						float dotProduct=DotProduct(L,N);

						if (dotProduct>0)
						{
							float diff = dotProduct*object->GetMaterial()->diffuse*shade;

							color+= diff * VectorProduct(object->GetMaterial()->color,light->GetMaterial()->color);
						}
					}

					if (object->GetMaterial()->specular>0)
					{
						vector3 V=ray.direction;
						vector3 R=L -2.0f*DotProduct(L,N)*N;
						float dotProduct=DotProduct(V,R);

						if (dotProduct>0)
						{
							float spec=powf(dotProduct,20)*object->GetMaterial()->specular*shade;

							color+=spec*light->GetMaterial()->color;
						}
					}
				}
			}
		}

		float reflact=object->GetMaterial()->reflaction;

		if (reflact>0.0f&&depth<MAXDEPTH)
		{

			float diffRefl=object->GetMaterial()->diffuseReflaction;

			/*if (diffRefl>0&&depth<2)
			{

				vector3 N=object->GetNormal(point);
				vector3 RP=ray.direction-2.0f*DotProduct(ray.direction,N)*N;
				vector3 RN1=vector3(RP.z,RP.y,-RP.x);
				vector3 RN2=CrossProduct(RP,RN1);

				reflact*=diffReflactScale;

			}else{*/
				
				vector3 N=object->GetNormal(point);
				vector3 R=ray.direction-2.0f*DotProduct(ray.direction,N)*N;

				vector3 colorTrace(0,0,0);
				float distanceTrace;
				Ray rayTrace;

				vector3 temp=point+R*EPSILON;
				rayTrace.SetOrigin(temp.x,temp.y,temp.z);
				rayTrace.SetDirection(R.x,R.y,R.z);

				RayTrace(rayTrace,colorTrace,depth+1,distanceTrace,index);
				color+=reflact*VectorProduct(colorTrace,object->GetMaterial()->color);

			//}
		}

		float refract= object->GetMaterial()->refraction;

		if (refract>0&&depth<MAXDEPTH)
		{
			float refrIndex=object->GetMaterial()->refractionIndex;
			float n=index/refrIndex;
			vector3 N=object->GetNormal(point)*(float)result;

			float cosI=-DotProduct(N,ray.direction);
			float cosII=1.0f-n*n*(1.0f-cosI*cosI);

			if (cosII>0.0f)
			{
				vector3 T=(n*ray.direction)+(n*cosI-sqrtf(cosII))*N;
				vector3 colorTrace(0,0,0);
				float distanceTrace;

				Ray rayTrace;

				vector3 temp=point+T*EPSILON;
				rayTrace.SetOrigin(temp.x,temp.y,temp.z);
				rayTrace.SetDirection(T.x,T.y,T.z);

				RayTrace(rayTrace,colorTrace,depth+1,distanceTrace,refrIndex);

				vector3 absorbance=object->GetMaterial()->color*0.15f*(-distanceTrace);
				vector3 transparency(expf(absorbance.x),expf(absorbance.y),expf(absorbance.z));

				color+=VectorProduct(colorTrace,transparency);
			}
		}
	}

	return object;
}

float Engine::CalculateShade( Object* light, vector3 point, vector3& L )
{
	float shade;

	Object* object;

	if (light->GetGeometryType()==SPHERE)
	{
		shade=1.0f;
		L=((Sphere*)light)->GetCenter()-point;

		float len=L.length();

		L*=(1.0f/len);

		Ray r;
		vector3 temp=point+L*EPSILON;
		r.SetOrigin(temp.x,temp.y,temp.z);
		r.SetDirection(L.x,L.y,L.z);

		for (int m=0;m<scene->GetObjectsNum();m++)
		{
			Object* occlusion=scene->GetObjectAt(m);

			if (occlusion->GetOpticalType()==NONLIGHT&&occlusion->IntersectTest(r,len))
			{
				shade=0.0f;
				break;
			}
		}
	}else if (light->GetGeometryType()==BOX)
	{
		shade=0.0f;
		Box* areaLight=(Box*)light;
		L=areaLight->GetPosition()+0.5f*areaLight->GetSize()-point;
		L=L.normalize();

		float sampleX=areaLight->GetSize().x*0.25f, sampleY=areaLight->GetSize().z*0.25f;

		for (int i=0;i<SAMPLES;i++)
		{
			vector3 samplePoint(areaLight->grids[(i & 15)<<1] + twister.Rand() * sampleX, areaLight->GetPosition().y, areaLight->grids[(( i & 15 )<<1)+1] + twister.Rand() * sampleY );
			vector3 dir = samplePoint - point;
			float len=dir.length();
			dir=dir.normalize();

			Ray r;
			vector3 temp=point+dir*EPSILON;
			r.SetOrigin(temp.x,temp.y,temp.z);
			r.SetDirection(dir.x,dir.y,dir.z);

			bool occlude=false;

			for (int m=0;m<scene->GetObjectsNum();m++)
			{
				Object* occlusion=scene->GetObjectAt(m);

				if (occlusion->GetOpticalType()==NONLIGHT&&occlusion->IntersectTest(r,len))
				{
					occlude=true;
					break;
				}
			}

			if (!occlude)
			{
				shade+=scale;
			}
		}
	}

	return shade;
}
inline vector3 VectorProduct( vector3& va, vector3& vb )
{
	return vector3(va.x*vb.x,va.y*vb.y,va.z*vb.z);
}

Box::Box( vector3& pos, vector3& size, int opt)
{
	position=pos;
	this->size=size;
	SetType(BOX);
	SetLight(opt);
	InitGrids();
}

void Box::InitGrids()
{
	grids=new float[32];
	grids[ 0] = 1, grids[ 1] = 2;
	grids[ 2] = 3, grids[ 3] = 3;
	grids[ 4] = 2, grids[ 5] = 0;
	grids[ 6] = 0, grids[ 7] = 1;
	grids[ 8] = 2, grids[ 9] = 3;
	grids[10] = 0, grids[11] = 3;
	grids[12] = 0, grids[13] = 0;
	grids[14] = 2, grids[15] = 2;
	grids[16] = 3, grids[17] = 1;
	grids[18] = 1, grids[19] = 3;
	grids[20] = 1, grids[21] = 0;
	grids[22] = 3, grids[23] = 2;
	grids[24] = 2, grids[25] = 1;
	grids[26] = 3, grids[27] = 0;
	grids[28] = 1, grids[29] = 1;
	grids[30] = 0, grids[31] = 2;

	for ( int i = 0; i < 16; i++ )
	{
		grids[i * 2] = grids[i * 2] *size.x / 4 + position.x;
		grids[i * 2 + 1] = grids[i * 2 + 1] * size.z / 4 + position.z;
	}

}

Triangle::Triangle( vector3& a, vector3& b, vector3& c, int opt )
{

	va=a;
	vb=b;
	vc=c;

	vector3 ba=b-a;
	vector3 cb=c-b;

	normal=CrossProduct(ba,cb).normalize();
	distance=DotProduct(normal,va);

	if (abs(normal.x)>abs(normal.y))
	{
		if (abs(normal.x)>abs(normal.z))
		{
			project=0;
		}else{
			project=2;
		}
	}else{
		if (abs(normal.y)>abs(normal.z))
		{
			project=1;
		}else{
			project=2;
		}
	}

	SetType(TRIANGLE);
	SetLight(opt);
}

vector3 Triangle::GetNormal( vector3& point )
{

	return normal;

}

int Triangle::IntersectTest( Ray& ray, float& dist )
{
	//dist=(distance-DotProduct(normal,ray.origin))/DotProduct(normal,ray.direction);
	float d=DotProduct(normal,ray.direction);

	vector3 pa, pb, pc, pi;

	if (d!=0)
	{
		float dis =-(DotProduct(normal,ray.origin)-distance)/d;

		if (dis > 0)
		{
			if (dis < dist) 
			{

				vector3 point=ray.origin+ray.direction*dis;

				switch (project)
				{
				case 0:
					pa.x=va.y;
					pa.y=va.z;
					pb.x=vb.y;
					pb.y=vb.z;
					pc.x=vc.y;
					pc.y=vc.z;
					pi.x=point.y;
					pi.y=point.z;
					break;

				case 1:
					pa.x=va.x;
					pa.y=va.z;
					pb.x=vb.x;
					pb.y=vb.z;
					pc.x=vc.x;
					pc.y=vc.z;
					pi.x=point.x;
					pi.y=point.z;
					break;

				case 2:
					pa.x=va.x;
					pa.y=va.y;
					pb.x=vb.x;
					pb.y=vb.y;
					pc.x=vc.x;
					pc.y=vc.y;
					pi.x=point.x;
					pi.y=point.y;
					break;
				}

				pa.z=pb.z=pc.z=pi.z=0;

				vector3 ai=pa-pi;
				vector3 bi=pb-pi;
				vector3 ci=pc-pi;

				vector3 ab=CrossProduct(ai,bi);
				vector3 bc=CrossProduct(bi,ci);
				vector3 ca=CrossProduct(ci,ai);

				if (ab.z<0&&bc.z<0&&ca.z<0)
				{
					dist = dis;
					return HIT;
				}				
			}
		}

	}
	return MISS;
}
